Skip to content

fix(core): read find() answers as QueryResult declares them in extractRecords - #8436

Merged
os-justin merged 1 commit into
mainfrom
claude/issue-6839-extract-records-arm-order
Sep 7, 2026
Merged

fix(core): read find() answers as QueryResult declares them in extractRecords#8436
os-justin merged 1 commit into
mainfrom
claude/issue-6839-extract-records-arm-order

Conversation

@os-justin

Copy link
Copy Markdown
Collaborator

Fixes #6839

Remove the records arm from the shared extractRecords normaliser
(packages/core/src/utils/extract-records.ts) and pin the contract read per
module. All work measured on 5dc25e3f5, the branch's only commit.

QueryResult (@object-ui/types) declares exactly one rows member, data.
The ladder was array -> records -> data -> value, i.e. the undeclared
spelling ahead of the contract's own member — the precedence inversion
#5945 was filed about and #6726 repaired by hand at seven other seams. It is
now array -> data -> value.


1. What the card got wrong, re-derived on this base

The card was measured on 26896c689 (#6726's base) and is stale in three
places. Every number below was re-derived on 0203a29e9.

card's claim today
"nine renderers call it directly" Ten call sites in nine packages, but only seven are direct. Four of the nine renderers now reach it through applyNonGridRowCeiling (@object-ui/react, #7210), which is itself a published export and an eleventh sink.
ObjectChart.tsx:196 (one site) two sites: :303 (group-by lookup labels) and :618 (the chart rows). The card names one.
ObjectTree:456, ObjectMap:714, ObjectCalendar:357 none of these files contains extractRecords any more — they reach it via applyNonGridRowCeiling at :561, :808, :490.
ObjectGantt:680, :1132 :752 (indirect) and :1277 (direct). Both still live.
ObjectKanban:298 / ObjectTimeline:213 / ObjectDataTable:680 :369 / :315 / :717.
ObjectPivotTable:157 :157, unchanged.
"the helper is genuinely dual-seam: ViewDataProvider also calls it" — the card's central complication, repeated by triage, and the stated reason not to just delete the arm FALSE, and false at filing time too. ViewDataProvider.ts:128 declares its own private extractRecords (records | data | items) and never imports the shared one. Verified on the card's own base: git show 26896c689:packages/core/src/data-scope/ViewDataProvider.ts has the same local declaration at the same line. The name collision was already recorded in this card's thread (#6917) the day after filing, and the triage comment five days later still asserted the dual seam.

No renderer had been fixed since filing — all nine are still live consumers.

⇒ The card's architecture question ("A: split it, or B: hand it rows") is
moot: there is one seam. No escalation is owed under the dispatch's fence —
the two arms are not both legitimate on different driver answers, because
nothing at this seam produces records at all.

2. Producer measurement, per consumer

Cell, stated: every object-literal records: key in every tracked
.ts/.tsx/.js/.jsx/.mjs file (143 hits / 103 files) — a complete population for
literal-form envelopes, with no body-scanner blind spot — plus three separate
sweeps for the shapes #6839's own thread warns a body-scanner cannot see:
mockResolvedValue-argument envelopes, brace-less arrow find bodies, and
computed/bracket-assigned keys.

Result: zero records envelopes reach any of the ten call sites. Per
package, the records: occurrences are doc comments, parameter names
(aggregateRecords(records: any[]), buildForest(records),
.then((records: any) =>), bare-array locals passed as
mockResolvedValue(records), and record-visibility batch route stubs
(fetch, not find).

Controls, so the zero is a reading and not a miss — the same sweeps are hot
elsewhere:

  • mockResolvedValue cell: subject records at a DataSource.find double = 0;
    control data = 102 files; control value = lit.
  • brace-less arrow find bodies: subject = 1 real hit,
    plugin-list/src/__tests__/ObjectGallery.expandFls-7429.test.tsx:117,
    find: vi.fn(async () => ({ records: [ROW] })). That producer feeds
    ListView's own unwrap ladder (ListView.tsx:2060-2067), which does not
    come through this helper. So the sweep demonstrably finds a producer of this
    exact shape when one exists.
  • computed keys: subject 0, control data lit.
  • records at the seams below the adapters is unchanged and untouched:
    ObjectStackAdapter.normalizeQueryResult, ApiDataSource .normalizeQueryResult (its ['data','items','results','records','value']
    loop), ViewDataProvider's private reader, apiAccessDenied.test.ts's
    client-SDK doubles, consoleUserPreferenceBudget.test.tsx's ds.client.data .find.

3. The value arm stays

#6840 / PR #6916 deleted value from ObjectView's ladder on a measured zero
at that seam and stated that its zero must not transfer here. Re-measured:
all five find() doubles it named still exist and still emit { value: [...] }
into this helper — plugin-kanban x3, plugin-calendar x2 (the calendar pair
now arrives through applyNonGridRowCeiling). Deleting it would break them.
That is a separate card with its own measurement.

QueryResult is not widened to bless records: published-type change,
maintainer's call, same floor #6726 and #6840 respected.

4. The pins — eleven files, 56 cases

One per module, named *.contractEnvelope-6839.*, in #6726/#6916's form: the
live arms are asserted alongside the deleted one, through the same mount,
because live and dead is the whole distinction.

pin what only it can see
core/.../extract-records.contractEnvelope-6839 the arm order. A render pin can only ask "did a records envelope paint rows"; only a direct call can ask "when BOTH keys are present, which wins".
react/.../nonGridRowCeiling.contractEnvelope-6839 a refused envelope also reports truncated: false over a 2001-row result set — it would have silenced the row-ceiling footnote, not merely drawn nothing.
ObjectChart (6 cases) two sites. Site 2's failure leaves every bar drawn and only swaps the axis label for a raw foreign key — invisible to any rows-only assertion.
ObjectGantt (6 cases) two sites, two routes. Site 2's failure leaves the chart intact and silently narrows the quick-filter dropdown to the values already on screen.
ObjectKanban, ObjectCalendar the live value arm, which is live only at these two packages.
ObjectDataTable, ObjectPivotTable siblings that settle differently: the table returns early to an empty-state panel, the pivot hands an empty array straight through as the memo's identity key.
ObjectTimeline, ObjectMap, ObjectTree the indirect route, at the rows / markers / forest rows.

5. Ablation — three legs, from the committed tree

Harness: absolute paths, trap ... EXIT INT TERM, restore by
git checkout HEAD -- path (never the bare form, which restores from the index
— i.e. from the mutation). Mutation proven on disk by printing the function body
and by hash; an empty hash is treated as failure, and a mutated hash equal to
the HEAD blob aborts the leg as a no-op. Restore proven by state: hash equal
to the HEAD blob AND git diff HEAD empty. No exit code is used as proof
anywhere.

git rev-parse HEAD:packages/core/src/utils/extract-records.ts =
d36ba54fe055c91ebe4afc91de8f10316ddc7bf4

Module resolution note: the root vitest.config.mts aliases @object-ui/core
to packages/core/src, so every leg resolves to source — no package
exports hop, no dist, and therefore no rebuild leg to get wrong.

Baseline (unmutated): VERDICT command-exit 0 — 11 files / 56 passed.

leg mutation mutated blob result
A put records back first (the bug) a9fd2799... VERDICT command-exit 1 — 11 files, 17 failed / 39 passed
B body replaced with return [] (worse than the bug) 3f319ecb... VERDICT command-exit 1 — 11 files, 35 failed / 21 passed
C reorder onlyrecords kept, moved behind data 163d668e... VERDICT command-exit 1 — 11 files, 15 failed / 41 passed

Restore verified after every leg: blob back to d36ba54f..., git diff HEAD
empty.

Leg A reddens by name, and only on the arm: every failing row is a
does NOT read records case, plus the three order cases —
data OUTRANKS records: a producer emitting both is read as QueryResult,
value outranks records too, and
a refused records envelope reports NOT truncated. Every live-arm case stayed
green.

Leg B answers the #8350 question directly: would an implementation strictly
worse than the bug pass these pins? No. A helper that returns [] for
everything reddens in all eleven files (35 cases), because every module has
at least one non-regression case pushing the same rows through the same mount.
The cases that stay green under B are exactly the refusal cases — which is the
proof they are not the whole pin.

Leg C separates "reorder" from "delete", the distinction the card's own
title invites. A mere reorder makes data OUTRANKS records green (the failing
list does not contain it) while all ten does NOT read records refusals stay
red, plus value outranks records too — because a reorder leaves the
undeclared spelling reachable whenever data is absent, which is precisely the
tolerance AGENTS.md #0.1 is about.

6. Verification (all at 5dc25e3f5)

Run through the container's shared heavy-verify lock; verdicts quoted from the
entry point, never a bare $?.

  • Full suites of the ten consumer packages, in two runs:
    os-verify-lock: VERDICT command-exit 0306 files / 4473 tests passed
    (core, react, plugin-charts, plugin-timeline, plugin-kanban) and
    VERDICT command-exit 0227 files / 1778 tests passed
    (plugin-dashboard, plugin-gantt, plugin-calendar, plugin-map, plugin-tree).
  • turbo run type-check over the same ten (tsc --noEmit && tsc -p tsconfig.test.json each): VERDICT command-exit 026/26 tasks. The new
    pins are proven to be in the program they are quoted from:
    tsc -p tsconfig.test.json --listFiles names
    extract-records.contractEnvelope-6839.test.ts (1 of 671) and
    ObjectGantt.contractEnvelope-6839.test.tsx (1 of 1486), each with a lit
    control and, for core, a negative control that is correctly absent.
  • turbo run lint over the same ten: VERDICT command-exit 0 — 11/11 tasks,
    0 errors.
  • Gates green: check-changeset-presence ("1 source file(s) of 1 released
    package(s) changed, and this change declares 1 changeset(s)"),
    check-changeset-no-major, type-check coverage (42/42 packages compile
    their tests), check-vi-mock-inherit, check-vi-mock-specifiers,
    check-control-bytes, check-phantom-dependencies,
    check-unused-dependencies, check-unreferenced-sources,
    check-package-self-import, check-lint-rule-coverage,
    check-element-data-source-declaration, check-comment-mask-corpus.
    check-governed-queue-guard --test on the 13 changed paths:
    NOT GOVERNED.

Not measured locally, declared to CI, and reported as NOT MEASURED rather
than as a pass or a failure
— three gates need a built tree this worktree does
not have, and each says so in its own words: check-readme-exports
("run pnpm build first"; its own census reports 21 unbuilt packages and its
population-collapse guard fires), check-eager-closure and
check-sdui-registration-pins (both "no console build ... this is a broken
gauge, not a passing budget"). None of the three names any file in this diff
(verified with a lit control on the same grep), and this diff changes no README,
no export surface and no bundle.

7. Out of scope, not fixed here

  • plugin-list's ListView has the same family of arm, and there it is
    LIVE.
    ListView.tsx:2060-2067 reads data -> records -> value on its own
    find() answer, and ObjectGallery.expandFls-7429.test.tsx:117 is a live
    find() double emitting { records: [ROW] } that depends on it. Different
    consumer, its own ladder, its own producer — untouched here. It also means the
    repo-wide phrasing "no producer emits records at the DataSource.find()
    seam" is consumer-local, not seam-wide: it is true of this helper's ten
    call sites and false of ListView. Reported to the PM for filing —
    search_issues is hard rate-limited for this session's account
    ("API rate limit already exceeded for user ID 323634890"), and the standing
    rule is not to file unsearched.
  • ViewDataProvider's private same-name extractRecords (a third tolerance
    set, records | data | items) is already filed as finding(fields,plugin-detail,plugin-list,app-shell): six more non-QueryResult count/value arms survive the seams #6840 repaired — and one of them reads value AHEAD of data #6917.

🤖 Generated with Claude Code

https://claude.ai/code/session_01YBWFb5YgMU5dw8p2VKj16S


Generated by Claude Code

…tRecords

Remove the `records` arm from the shared `extractRecords` normaliser and pin
the contract read per module.

`QueryResult` (`@object-ui/types`) declares exactly one rows member, `data`.
The ladder was `array -> records -> data -> value`, i.e. the undeclared
spelling AHEAD of the contract's own member — the precedence inversion
objectui#5945 was filed about and objectui#6726 repaired by hand at seven other
seams. `records` is the below-the-adapter spelling that
`ObjectStackAdapter.normalizeQueryResult` and `ApiDataSource
.normalizeQueryResult` already fold into `data` below every consumer.

Reach re-derived on this tree: ten call sites in nine packages. Seven direct
(ObjectChart x2, ObjectDataTable, ObjectPivotTable, ObjectGantt, ObjectKanban,
ObjectTimeline) and four through `applyNonGridRowCeiling` (`@object-ui/react`),
itself a published export and a sink of its own (ObjectCalendar, ObjectGantt's
rows, ObjectMap, ObjectTree).

Producer measurement per consumer: no `find()` in those nine packages, nor in
the apps and examples mounting them, emits a `records` envelope. Control — the
same sweep finds `records` envelopes on other seams, including one live `find()`
double at `plugin-list`'s ObjectGallery, a consumer with its own unwrap ladder,
untouched here.

The `value` arm stays: five `find()` doubles feed it today (plugin-kanban x3,
plugin-calendar x2), and objectui#6840 explicitly refused to transfer its own
zero to this seam. `QueryResult` is NOT widened.

One refusal pin per module (`*.contractEnvelope-6839.*`), each keeping the live
arms green alongside the deleted one, plus a direct pin on the helper for the
precedence question a per-module render pin cannot ask.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YBWFb5YgMU5dw8p2VKj16S
@github-actions

github-actions Bot commented Sep 7, 2026

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 50 chunks) 3472.8 KB 3512.7 KB
Main entry chunk (gzip) 143.9 KB 350 KB
Entry file index-BkdUgUqM.js
Status PASS

The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it.


📦 Bundle Size Report

Package Size Gzipped
app-shell (consoleActionDispatch.js) 0.20KB 0.19KB
app-shell (index.js) 15.67KB 5.75KB
app-shell (runtime-config.js) 20.68KB 7.36KB
app-shell (types.js) 0.01KB 0.04KB
app-shell (urlParams.js) 10.06KB 3.86KB
auth (ActiveOrganizationStorage.js) 25.05KB 9.16KB
auth (AuthContext.js) 0.31KB 0.24KB
auth (AuthGuard.js) 2.07KB 1.00KB
auth (AuthProvider.js) 40.18KB 10.59KB
auth (AuthShell.js) 3.49KB 1.40KB
auth (ForgotPasswordForm.js) 12.21KB 3.45KB
auth (LoginForm.js) 18.15KB 5.39KB
auth (PreviewBanner.js) 0.90KB 0.50KB
auth (RegisterForm.js) 6.65KB 2.22KB
auth (SocialSignInButtons.js) 9.61KB 3.89KB
auth (UserMenu.js) 3.41KB 1.23KB
auth (auth-gate-events.js) 1.29KB 0.66KB
auth (authStyles.js) 5.04KB 1.72KB
auth (createAuthClient.js) 40.21KB 10.80KB
auth (createAuthenticatedFetch.js) 8.46KB 3.43KB
auth (index.js) 3.19KB 1.44KB
auth (invitation-status.js) 1.22KB 0.70KB
auth (org-roles.js) 6.66KB 2.78KB
auth (phone-identifier.js) 1.11KB 0.66KB
auth (types.js) 0.59KB 0.35KB
auth (useAuth.js) 5.30KB 1.02KB
auth (useWorkspaceAdminStatus.js) 11.08KB 4.58KB
collaboration (CommentThread.js) 26.08KB 7.56KB
collaboration (LiveCursors.js) 3.17KB 1.27KB
collaboration (PresenceAvatars.js) 6.49KB 2.64KB
collaboration (PresenceProvider.js) 2.79KB 1.13KB
collaboration (index.js) 1.68KB 0.73KB
collaboration (useCollaborationTranslation.js) 6.05KB 2.52KB
collaboration (useCommentSearch.js) 1.98KB 0.88KB
collaboration (useConflictResolution.js) 7.75KB 1.86KB
collaboration (useMentionNotifications.js) 1.81KB 0.68KB
collaboration (usePresence.js) 6.33KB 1.84KB
collaboration (useRealtimeSubscription.js) 7.91KB 2.01KB
components (index.js) 498.55KB 114.03KB
core (index.js) 7.48KB 2.96KB
create-plugin (index.js) 10.12KB 3.28KB
data-objectstack (index.js) 189.15KB 52.56KB
fields (index.js) 243.15KB 61.40KB
i18n (LocalizationContext.js) 1.76KB 0.96KB
i18n (builtinAggregateLabels.js) 0.86KB 0.49KB
i18n (currency.js) 1.22KB 0.64KB
i18n (fallbackInterpolation.js) 6.25KB 2.77KB
i18n (i18n.js) 6.57KB 2.76KB
i18n (index.js) 3.65KB 1.47KB
i18n (pickLocalized.js) 7.62KB 3.26KB
i18n (provider.js) 26.89KB 9.04KB
i18n (useDisplayLocale.js) 2.85KB 1.45KB
i18n (useObjectLabel.js) 34.34KB 9.17KB
i18n (useSafeTranslation.js) 5.60KB 2.33KB
layout (index.js) 38.84KB 10.94KB
mobile (MobileProvider.js) 0.92KB 0.49KB
mobile (ResponsiveContainer.js) 0.94KB 0.38KB
mobile (breakpoints.js) 1.51KB 0.70KB
mobile (createOfflineDataSource.js) 5.61KB 1.75KB
mobile (index.js) 1.99KB 0.87KB
mobile (offlineQueue.js) 3.91KB 1.35KB
mobile (pwa.js) 0.97KB 0.49KB
mobile (serviceWorker.js) 1.48KB 0.62KB
mobile (serviceWorkerSource.js) 3.41KB 1.48KB
mobile (useBreakpoint.js) 1.54KB 0.65KB
mobile (useGesture.js) 6.96KB 1.98KB
mobile (useOfflineSync.js) 1.99KB 0.72KB
mobile (usePullToRefresh.js) 2.53KB 0.85KB
mobile (useResponsive.js) 0.72KB 0.42KB
mobile (useSpecGesture.js) 4.39KB 1.66KB
mobile (useTouchTarget.js) 1.01KB 0.54KB
permissions (MePermissionsProvider.js) 11.71KB 4.29KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 6.24KB 2.16KB
permissions (discardProofCache.js) 1.04KB 0.55KB
permissions (evaluator.js) 5.12KB 1.74KB
permissions (index.js) 0.93KB 0.41KB
permissions (store.js) 0.91KB 0.42KB
permissions (useFieldPermissions.js) 1.28KB 0.53KB
permissions (usePermissions.js) 4.83KB 2.27KB
plugin-ai (index.js) 15.16KB 3.68KB
plugin-calendar (index.js) 49.00KB 13.91KB
plugin-charts (index.js) 71.39KB 19.92KB
plugin-chatbot (index.js) 194.52KB 46.34KB
plugin-dashboard (index.js) 131.48KB 34.45KB
plugin-designer (index.js) 213.21KB 43.63KB
plugin-detail (index.js) 248.68KB 63.94KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 131.01KB 32.32KB
plugin-gantt (index.js) 167.16KB 40.99KB
plugin-grid (index.js) 208.58KB 56.63KB
plugin-kanban (index.js) 55.38KB 15.72KB
plugin-list (index.js) 112.74KB 27.70KB
plugin-map (index.js) 20.49KB 6.83KB
plugin-markdown (index.js) 13.88KB 4.80KB
plugin-report (index.js) 43.42KB 11.92KB
plugin-timeline (index.js) 30.10KB 8.74KB
plugin-tree (index.js) 9.33KB 3.25KB
plugin-view (index.js) 84.54KB 20.84KB
providers (DataSourceProvider.js) 0.75KB 0.39KB
providers (MetadataProvider.js) 1.37KB 0.59KB
providers (ThemeProvider.js) 1.90KB 0.85KB
providers (UploadProvider.js) 11.66KB 3.50KB
providers (index.js) 0.45KB 0.23KB
providers (types.js) 0.01KB 0.04KB
react-runtime (index.js) 5.62KB 2.34KB
react (LazyPluginLoader.js) 4.47KB 1.63KB
react (SchemaRenderer.js) 81.07KB 26.86KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 4.63KB 2.18KB
react (schema-input.js) 2.32KB 1.24KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (codegen.js) 6.58KB 2.74KB
sdui-parser (dashboard-widget-options.js) 3.08KB 1.30KB
sdui-parser (index.js) 5.55KB 2.45KB
sdui-parser (input-type.js) 2.84KB 1.40KB
sdui-parser (parse.js) 20.57KB 5.88KB
sdui-parser (provenance.js) 3.66KB 1.82KB
sdui-parser (types.js) 0.28KB 0.23KB
sdui-parser (validate.js) 13.64KB 4.59KB
types (ai.js) 0.20KB 0.17KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 2.87KB 1.00KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (complex.js) 2.93KB 1.49KB
types (crud.js) 0.20KB 0.18KB
types (dashboard-filter-alias.js) 6.23KB 2.74KB
types (data-display.js) 3.75KB 1.85KB
types (data-protocol.js) 0.20KB 0.19KB
types (data.js) 0.20KB 0.18KB
types (designer.js) 1.85KB 0.85KB
types (disclosure.js) 0.20KB 0.18KB
types (error-code.js) 1.54KB 0.88KB
types (expression.js) 0.20KB 0.18KB
types (feedback.js) 0.20KB 0.18KB
types (field-types.js) 0.20KB 0.18KB
types (form.js) 0.20KB 0.18KB
types (http-inflight.js) 8.87KB 3.73KB
types (http-retry.js) 4.32KB 2.02KB
types (icon-key-migration.js) 4.26KB 1.63KB
types (index.js) 4.74KB 2.25KB
types (layout.js) 0.20KB 0.18KB
types (managed-by.js) 0.19KB 0.18KB
types (mobile.js) 4.73KB 2.28KB
types (navigation.js) 0.20KB 0.18KB
types (objectql.js) 0.20KB 0.18KB
types (overlay.js) 0.20KB 0.18KB
types (permissions.js) 0.20KB 0.18KB
types (plugin-scope.js) 0.20KB 0.18KB
types (record-components.js) 0.20KB 0.19KB
types (record-semantics.js) 1.28KB 0.67KB
types (registry.js) 0.20KB 0.18KB
types (reports.js) 0.20KB 0.18KB
types (select-option.js) 0.20KB 0.19KB
types (spec-report.js) 5.05KB 1.93KB
types (spec-ui-namespace.js) 0.20KB 0.19KB
types (system-fields.js) 3.33KB 1.54KB
types (theme.js) 6.28KB 2.87KB
types (ui-action.js) 8.11KB 3.32KB
types (views.js) 0.20KB 0.18KB
types (widget.js) 0.20KB 0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

@os-justin
os-justin added this pull request to the merge queue Sep 7, 2026
Merged via the queue into main with commit a2d2515 Sep 7, 2026
34 checks passed
@os-justin
os-justin deleted the claude/issue-6839-extract-records-arm-order branch September 7, 2026 23:54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

extractRecords reads records AHEAD of data on DataSource.find() answers — the shared-helper copy of the #5945/#6726 inversion, live in 9 renderers

2 participants